package sideeffect

import (
	"context"
	"encoding/json"
	errors2 "errors"
	"fmt"
	"github.com/mitchellh/hashstructure"
	"gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
	"gitverse.ru/synapse/kubelatte/pkg/observability/logs"
	"gitverse.ru/synapse/kubelatte/pkg/operator"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"strconv"
	"strings"
)

type parentStatus int8

const (
	unknown = iota
	notYetUpdated
	publishedHashChanged
	publishedHashNotChanged
	obtained
)

func (e *Executor) applySideEffectRecreate(ctx context.Context, info *SE) {
	var oldTI []*v1alpha1.TriggerInstance
	var err error

	logs.Debugf("side effect starting; sideEffectConfig: %v; triggerName: %v; namespace: %v",
		info.Configs.TriggerRef.SideEffectConfig, info.Configs.TriggerRef.Name, info.Configs.Metadata.Namespace)

	ns := info.Configs.Metadata.Namespace
	sideEffConfig := info.Configs.TriggerRef.SideEffectConfig

	e.mu.Lock()
	defer e.mu.Unlock()

	oldTI, err = e.getOldTIs(ctx, sideEffConfig, info.TriggerRef.Name, info.TriggerRef.Namespace, ns)
	if err != nil {
		return
	}

	oldParentHash := oldTI[0].GetObjectMeta().GetAnnotations()[annotParentHash]
	if oldParentHash == "" {
		logs.Error("oldParentHash is empty")
		e.deleteTIs(ctx, oldTI, ns)
		e.applySideEffectCreate(ctx, info)
		return
	}

	parentByte, err := e.waitForUpdatedParentPublication(ctx, oldParentHash, info)
	if err != nil {
		return
	}

	parent, err := e.getResourceInfo(ctx, parentByte)
	if err != nil {
		return
	}
	e.deleteTIs(ctx, oldTI, ns)
	e.createTIsByTemplateRefs(ctx, info, parent)
}

func (e *Executor) getOldTIs(ctx context.Context, secName, trig, trigNs, ns string) ([]*v1alpha1.TriggerInstance, error) { // sec == sideEffectConfig
	var tis []*v1alpha1.TriggerInstance

	triggerInstances, err := e.kbtlKubeClient.TriggerInstance().List(ctx, ns, metav1.ListOptions{})
	if err != nil {
		logs.Errorf("failed list triggerInstances %s", err)
		return nil, err
	}

	for i := 0; i < len(triggerInstances.Items); i++ {
		s, ok := triggerInstances.Items[i].Annotations[annotSideEffConfigName]
		if !ok || s != secName {
			continue
		}
		tns, ok := triggerInstances.Items[i].Annotations[annotTriggerNamespace]
		if !ok || tns != trigNs {
			continue
		}
		t, ok := triggerInstances.Items[i].Annotations[annotTriggerName]
		if !ok || t != trig {
			continue
		}
		tis = append(tis, &triggerInstances.Items[i])
	}

	if len(tis) == 0 {
		return nil, errors2.New("no tis found")
	}

	return tis, nil
}

func (e *Executor) waitForUpdatedParentPublication(ctx context.Context, oldParentHash string, info *SE) ([]byte, error) {
	isParentUpdatedFunc := func(parentByte []byte, expectedTag int64) parentStatus {
		var tmp interface{}

		err := json.Unmarshal(parentByte, &tmp)
		if err != nil {
			logs.Errorf("json unmarshalling error: %s, tmp %s", err.Error(), string(parentByte))
			return unknown
		}
		obj, ok := tmp.(map[string]any)
		if !ok {
			logs.Errorf("getting object error: tmp %s", string(parentByte))
			return unknown
		}

		tag := GetTag(ctx, obj)
		if tag < expectedTag {
			return notYetUpdated
		}
		if tag > expectedTag {
			return obtained
		}

		objHash, err := e.getObjControlFieldsHash(ctx, obj)
		if err != nil || objHash == "" {
			return unknown
		}

		if objHash != oldParentHash {
			return publishedHashChanged
		}
		return publishedHashNotChanged
	}

	parentByte, status, err := e.waitForParentPublication(ctx, operator.TimeoutParentWaiting, info, isParentUpdatedFunc)
	if err != nil && !errors.IsNotFound(err) {
		logs.Errorf("failed to check resource in: ns %s; kind %s; resource %s; error %s",
			info.Configs.Metadata.Namespace, info.Configs.Kind, info.Configs.Metadata.Name, err.Error())
		return nil, err
	}

	switch status {
	case publishedHashChanged:
		break
	case obtained:
		msg := fmt.Sprintf("resource %v:%v ns %v is obtained, side effect recreation will not be applied", info.Configs.Kind, info.Configs.Metadata.Name, info.Configs.Metadata.Namespace)
		logs.Info(msg)
		return nil, errors2.New(msg)
	case publishedHashNotChanged:
		msg := fmt.Sprintf("resource %v:%v ns %v was not updated in control fields, side effect recreation will not be applied", info.Configs.Kind, info.Configs.Metadata.Name, info.Configs.Metadata.Namespace)
		logs.Infof(msg)
		return nil, errors2.New(msg)
	case unknown:
		msg := fmt.Sprintf("resource %v:%v ns %v was not updated inside timeout, side effect recreation will not be applied", info.Configs.Kind, info.Configs.Metadata.Name, info.Configs.Metadata.Namespace)
		logs.Infof(msg)
		return nil, errors2.New(msg)
	case notYetUpdated:
		msg := fmt.Sprintf("resource %v:%v ns %v was not updated inside timeout, side effect recreation will not be applied", info.Configs.Kind, info.Configs.Metadata.Name, info.Configs.Metadata.Namespace)
		logs.Infof(msg)
		return nil, errors2.New(msg)
	}

	return parentByte, nil
}

type ControlFields struct {
	Annotations map[string]any
	Labels      map[string]any
	Spec        any
	Data        any
}

func (e *Executor) getObjControlFieldsHash(ctx context.Context, obj map[string]any) (string, error) {
	metadata := obj["metadata"].(map[string]any)
	labels := getMapElsWithPrefix(metadata, "labels")
	annots := getMapElsWithPrefix(metadata, "annotations")

	hash, err := hashstructure.Hash(ControlFields{
		Annotations: annots,
		Labels:      labels,
		Spec:        obj["spec"],
		Data:        obj["data"],
	}, nil)
	if err != nil {
		logs.Errorf("hash for obj %s:%s can not be created: %s", obj["kind"].(string), metadata["name"].(string), err.Error())
	}
	return strconv.FormatUint(hash, 10), err
}

func getMapElsWithPrefix(source map[string]any, targetMapName string) map[string]any {
	res := make(map[string]any)

	tmp, ok := source[targetMapName]
	if !ok {
		return nil
	}

	targetMap, ok := tmp.(map[string]any)
	if !ok {
		return nil
	}

	for k, v := range targetMap {
		if strings.HasPrefix(k, prefix+"/") {
			res[k] = v
		}
	}

	return res
}

func (e *Executor) deleteTIs(ctx context.Context, tis []*v1alpha1.TriggerInstance, ns string) {
	for _, ti := range tis {
		err := e.kbtlKubeClient.TriggerInstance().Delete(ctx, ns, ti.Name, metav1.DeleteOptions{})
		if err != nil {
			logs.Errorf("failed delete triggerInstance %s: %s", ti.Name, err)
		}
	}
}
